/*
* Copyright (C) 2008-2009 J-P Nurmi jpnurmi@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* $Id: messageview.cpp 398 2009-05-09 10:40:02Z jpnurmi $
*/

#include "messageview.h"
#include "nickhighlighter.h"
#include "ircmodelitem.h"
#include "topiclabel.h"
#include <QDesktopServices>
#include <QCompleter>
#include <QScrollBar>
#include <QShortcut>
#include <QKeyEvent>
#include <QTime>
#include <QMenu>

static QRegExp URL_PATTERN("((www\\.(?!\\.)|(ssh|fish|irc|(f|sf|ht)tp(|s))://)(\\.?[\\d\\w/,\\':~\\?=;#@\\-\\+\\%\\*\\{\\}\\!\\(\\)]|&)+)|([-.\\d\\w]+@[-.\\d\\w]{2,}\\.[\\w]{2,})", Qt::CaseInsensitive);

static QString messageColor(int color)
{
    switch (color)
    {
        case 0:  return QLatin1String("white");
        case 1:  return QLatin1String("black");
        case 2:  return QLatin1String("navy");
        case 3:  return QLatin1String("green");
        case 4:  return QLatin1String("red");
        case 5:  return QLatin1String("maroon");
        case 6:  return QLatin1String("purple");
        case 7:  return QLatin1String("orange");
        case 8:  return QLatin1String("yellow");
        case 9:  return QLatin1String("lime");
        case 10: return QLatin1String("darkcyan");
        case 11: return QLatin1String("cyan");
        case 12: return QLatin1String("blue");
        case 13: return QLatin1String("magenta");
        case 14: return QLatin1String("gray");
        case 15: return QLatin1String("lightgray");
        default: return QLatin1String("black");
    }
}

QString MessageView::prefixedSender(const QString& sender) const
{
    if (!d.prefixCache.isEmpty() && d.prefixCache.mid(1) == sender)
        return d.prefixCache;

    d.prefixCache = sender;
    QModelIndex index = model()->match(model()->index(0,0), IrcModelItem::NameRole, sender, 1, Qt::MatchFixedString).value(0);
    if (index.isValid())
        d.prefixCache = index.data(Qt::DisplayRole).toString();
    return d.prefixCache;
}

QString MessageView::processSender(const QString& sender) const
{
    if (sender.isEmpty() || sender.startsWith("#") || sender.startsWith("&"))
        return sender;

    QString prefixed = prefixedSender(sender);
    return QString("<a href='query:%1'>%2</a>").arg(sender).arg(prefixed);
}

QString MessageView::processMessage(const QString& message) const
{
    QString processed = message;
    processed.replace("<", "&lt;");
    processed.replace(">", "&gt;");

    enum
    {
        None            = 0x0,
        Bold            = 0x1,
        Color           = 0x2,
        Italic          = 0x4,
        StrikeThrough   = 0x8,
        Underline       = 0x10,
        Inverse         = 0x20
    };
    int state = None;

    int pos = 0;
    while (pos < processed.size())
    {
        if (state & Color)
        {
            QString tmp = processed.mid(pos, 2);
            processed.remove(pos, 2);
            processed = processed.arg(messageColor(tmp.toInt()));
            state &= ~Color;
            pos += 2;
            continue;
        }

        QString replacement;
        switch (processed.at(pos).unicode())
        {
            case '\x02': // bold
                if (state & Bold)
                    replacement = QLatin1String("</span>");
                else
                    replacement = QLatin1String("<span style='font-weight: bold'>");
                state ^= Bold;
                break;

            case '\x03': // color
                if (state & Color)
                    replacement = QLatin1String("</span>");
                else
                    replacement = QLatin1String("<span style='color: %1'>");
                state ^= Color;
                break;

            case '\x09': // italic
                if (state & Italic)
                    replacement = QLatin1String("</span>");
                else
                    replacement = QLatin1String("<span style='text-decoration: underline'>");
                state ^= Italic;
                break;

            case '\x13': // strike-through
                if (state & StrikeThrough)
                    replacement = QLatin1String("</span>");
                else
                    replacement = QLatin1String("<span style='text-decoration: line-through'>");
                state ^= StrikeThrough;
                break;

            case '\x15': // underline
            case '\x1f': // underline
                if (state & Underline)
                    replacement = QLatin1String("</span>");
                else
                    replacement = QLatin1String("<span style='text-decoration: underline'>");
                state ^= Underline;
                break;

            case '\x16': // inverse
                state ^= Inverse;
                break;

            case '\x0f': // none
                replacement = QLatin1String("</span>");
                state = None;
                break;

            default:
                break;
        }

        if (!replacement.isNull())
        {
            processed.replace(pos, 1, replacement);
            pos += replacement.length();
        }
        else
        {
            ++pos;
        }
    }

    pos = 0;
    while ((pos = URL_PATTERN.indexIn(processed, pos)) >= 0)
    {
        int len = URL_PATTERN.matchedLength();
        QString href = processed.mid(pos, len);

        // Don't consider trailing &gt; as part of the link.
        QString append;
        if (href.endsWith("&gt;"))
        {
            append.append(href.right(4));
            href.chop(4);
        }

        // Don't consider trailing comma or semi-colon as part of the link.
        if (href.endsWith(",") || href.endsWith(";"))
        {
            append.append(href.right(1));
            href.chop(1);
        }

        // Don't consider trailing closing parenthesis part of the link when
        // there's an opening parenthesis preceding in the beginning of the
        // URL or there is no opening parenthesis in the URL at all.
        if (pos > 0 && href.endsWith(")") && (processed.at(pos-1) == '(' || !href.contains("(")))
        {
            append.prepend(href.right(1));
            href.chop(1);
        }

        // Qt doesn't support (?<=pattern) so we do it here
        if (pos > 0 && processed.at(pos-1).isLetterOrNumber())
        {
            pos++;
            continue;
        }

        QString protocol;
        if (URL_PATTERN.cap(1).startsWith("www.", Qt::CaseInsensitive))
            protocol = "http://";
        else if (URL_PATTERN.cap(1).isEmpty())
            protocol = "mailto:";

        // Use \x0b as a placeholder for & so we can readd them after changing all & in the normal text to &amp;
        // HACK Replace % with \x03 in the url to keep Qt from doing stupid things
        QString link = QString("<a href='%1%2'>%3</a>").arg(protocol, QString(href).replace('&', "\x0b").replace('%', "\x03"), href) + append;
        processed.replace(pos, len, link);
        pos += link.length();
    }

    return processed;
}

MessageView::MessageView(const QString& receiver, QWidget* parent)
    : QWidget(parent)
{
    d.setupUi(this);

    setFocusProxy(d.lineEdit);
    d.textBrowser->installEventFilter(this);
    d.textBrowser->viewport()->installEventFilter(this);
    d.highlighter = new NickHighlighter(d.textBrowser->document());
    connect(d.textBrowser, SIGNAL(anchorClicked(QUrl)), this, SLOT(onAnchorClicked(QUrl)));

    d.completer = new QCompleter(this);
    d.completer->setWidget(this);
    d.completer->setCaseSensitivity(Qt::CaseInsensitive);
    d.completer->setCompletionMode(QCompleter::InlineCompletion);
    d.completer->setCompletionRole(IrcModelItem::NameRole);
    connect(d.completer, SIGNAL(highlighted(QString)), this, SLOT(insertCompletion(QString)));

    connect(d.lineEdit, SIGNAL(textEdited(QString)), this, SLOT(onTextEdited()));
    connect(d.lineEdit, SIGNAL(returnPressed()), this, SLOT(onSend()));
    connect(d.pushButton, SIGNAL(clicked()), this, SLOT(onSend()));

    QShortcut* shortcut = new QShortcut(Qt::Key_Tab, this);
    connect(shortcut, SIGNAL(activated()), this, SLOT(onTabPressed()));

    shortcut = new QShortcut(QKeySequence::MoveToPreviousPage, this);
    connect(shortcut, SIGNAL(activated()), this, SLOT(moveToPrevPage()));

    shortcut = new QShortcut(QKeySequence::MoveToNextPage, this);
    connect(shortcut, SIGNAL(activated()), this, SLOT(moveToNextPage()));

    shortcut = new QShortcut(QKeySequence::Find, this);
    connect(shortcut, SIGNAL(activated()), this, SLOT(find()));

    shortcut = new QShortcut(QKeySequence::FindNext, this);
    connect(shortcut, SIGNAL(activated()), this, SLOT(findNext()));
    connect(d.toolNext, SIGNAL(clicked()), this, SLOT(findNext()));
    connect(d.editFind, SIGNAL(returnPressed()), this, SLOT(findNext()));
    connect(d.editFind, SIGNAL(textEdited(QString)), this, SLOT(find(QString)));

    shortcut = new QShortcut(QKeySequence::FindPrevious, this);
    connect(shortcut, SIGNAL(activated()), this, SLOT(findPrevious()));
    connect(d.toolPrevious, SIGNAL(clicked()), this, SLOT(findPrevious()));

    shortcut = new QShortcut(Qt::Key_Escape, this);
    connect(shortcut, SIGNAL(activated()), d.frameFind, SLOT(hide()));
    connect(d.toolClose, SIGNAL(clicked()), d.frameFind, SLOT(hide()));

    d.label->setVisible(false);
    d.frameFind->setVisible(false);
    d.labelWrapped->setVisible(false);

    setReceiver(receiver);
}

MessageView::~MessageView()
{
}

QString MessageView::receiver() const
{
    return d.receiver;
}

void MessageView::setReceiver(const QString& receiver)
{
    d.receiver = receiver;
    setWindowTitle(receiver);
}

uint MessageView::getId() const
{
    return d.id;
}

void MessageView::setId(uint id)
{
    d.id = id;
}

bool MessageView::matches(const QString& receiver) const
{
    return !d.receiver.compare(receiver, Qt::CaseInsensitive);
}

bool MessageView::contains(const QString& sender) const
{
    QAbstractItemModel* model = d.completer->model();
    if (!model)
        return false;

    return !model->match(model->index(0,0), Qt::DisplayRole, sender).isEmpty();
}

QAbstractItemModel* MessageView::model() const
{
    return d.completer->model();
}

void MessageView::setModel(QAbstractItemModel* model)
{
    d.completer->setModel(model);
}

void MessageView::receiveHelp(const QString& message)
{
    logMessage(QString(), "%1? %2 ", message);
}

void MessageView::receiveMessage(const QString& sender, const QString& message, bool highlight)
{
    logMessage(sender, "&lt;%1&gt; %2", message, highlight);
}

void MessageView::receiveNotice(const QString& sender, const QString& message, bool highlight)
{
    logMessage(sender, "[%1] %2", message, highlight);
}

void MessageView::receiveAction(const QString& sender, const QString& message, bool highlight)
{
    logMessage(sender, "* %1 %2", message, highlight);
}

void MessageView::logMessage(const QString& sender, const QString& format, const QString& message, bool highlight)
{
    QString msg = format.arg(processSender(sender)).arg(processMessage(message));
    bool blue = msg.startsWith("! ");
    bool brown = msg.startsWith("? ");
    bool magenta = msg.startsWith("* ");

    QString time = QTime::currentTime().toString();
    msg = QString("[%1] %2").arg(time).arg(msg);

    if (highlight)
        msg = QString("<span style='color:red'>%1</span>").arg(msg);
    else if (blue)
        msg = QString("<span style='color:steelblue'>%1</span>").arg(msg);
    else if (brown)
        msg = QString("<span style='color:brown'>%1</span>").arg(msg);
    else if (magenta)
        msg = QString("<span style='color:darkmagenta'>%1</span>").arg(msg);

    d.textBrowser->append(msg);
    if (!sender.isEmpty() && !sender.startsWith("#") && !sender.startsWith("&"))
    {
        QTextBlock block = d.textBrowser->document()->lastBlock();
        TextBlockUserData* data = dynamic_cast<TextBlockUserData*>(block.userData());
        if (!data)
            data = new TextBlockUserData;
        data->sender = prefixedSender(sender);
        block.setUserData(data);
    }
}

void MessageView::setTopic(const QString& topic)
{
    d.label->setTopic(processMessage(topic));
}

int MessageView::zoomLevel() const
{
    return d.textBrowser->font().pointSize();
}

void MessageView::setZoomLevel(int level)
{
    QFont font = d.textBrowser->font();
    font.setPointSize(level);
    d.textBrowser->setFont(font);
}

void MessageView::zoomIn()
{
    d.textBrowser->zoomIn();
}

void MessageView::zoomOut()
{
    d.textBrowser->zoomOut();
}

void MessageView::clear()
{
    d.textBrowser->clear();
}

void MessageView::findNext()
{
    find(d.editFind->text(), true, false);
}

void MessageView::findPrevious()
{
    find(d.editFind->text(), false, true);
}

void MessageView::find()
{
    d.frameFind->show();
    d.editFind->setFocus(Qt::ShortcutFocusReason);
    d.editFind->selectAll();
}

void MessageView::find(const QString& text, bool forward, bool backward)
{
    QTextDocument* doc = d.textBrowser->document();
    QTextCursor cursor = d.textBrowser->textCursor();
    QString oldText = d.editFind->text();

    QTextDocument::FindFlags options;
    QPalette pal = d.editFind->palette();
    pal.setColor(QPalette::Active, QPalette::Base, Qt::white);

    if (cursor.hasSelection())
        cursor.setPosition(forward ? cursor.position() : cursor.anchor(), QTextCursor::MoveAnchor);

    QTextCursor newCursor = cursor;

    if (!text.isEmpty()) {
        if (backward)
            options |= QTextDocument::FindBackward;

        if (d.checkCase->isChecked())
            options |= QTextDocument::FindCaseSensitively;

        if (d.checkWholeWords->isChecked())
            options |= QTextDocument::FindWholeWords;

        newCursor = doc->find(text, cursor, options);
        d.labelWrapped->hide();

        if (newCursor.isNull()) {
            QTextCursor ac(doc);
            ac.movePosition(options & QTextDocument::FindBackward
                ? QTextCursor::End : QTextCursor::Start);
            newCursor = doc->find(text, ac, options);
            if (newCursor.isNull()) {
                pal.setColor(QPalette::Active, QPalette::Base, QColor(255, 102, 102));
                newCursor = cursor;
            } else
                d.labelWrapped->show();
        }
    }

    if (!d.frameFind->isVisible())
        d.frameFind->show();
    d.textBrowser->setTextCursor(newCursor);
    d.editFind->setPalette(pal);
}

bool MessageView::eventFilter(QObject* receiver, QEvent* event)
{
    Q_UNUSED(receiver);
    if (event->type() == QEvent::KeyPress)
    {
        QKeyEvent* keyEvent =  static_cast<QKeyEvent*>(event);
        // for example:
        // - Ctrl+C goes to the browser
        // - Ctrl+V goes to the line edit
        // - Shift+7 ("/") goes to the line edit
        switch (keyEvent->key())
        {
        case Qt::Key_Shift:
        case Qt::Key_Control:
        case Qt::Key_Meta:
        case Qt::Key_Alt:
        case Qt::Key_AltGr:
            break;
        default:
            if (!keyEvent->matches(QKeySequence::Copy))
            {
                QApplication::sendEvent(d.lineEdit, keyEvent);
                d.lineEdit->setFocus();
                return true;
            }
            break;
        }
    }
    else if (event->type() == QEvent::ContextMenu)
    {
        QContextMenuEvent* contextMenuEvent = static_cast<QContextMenuEvent*>(event);
        QString anchor = d.textBrowser->anchorAt(contextMenuEvent->pos());
        if (anchor.startsWith("query:"))
        {
            QMenu menu(this);
            QAction* action = 0;
            QString nick = QUrl(anchor).path();

            action = menu.addAction(tr("&Whois"), this, SLOT(onActionTriggered()));
            action->setData(QString("WHOIS %1 %1").arg(nick));

            action = menu.addAction(tr("&Version"), this, SLOT(onActionTriggered()));
            action->setData(QString("PRIVMSG %1 :\x01VERSION\x01").arg(nick));

            action = menu.addAction(tr("&Ping"), this, SLOT(onActionTriggered()));
            action->setData(QString("PRIVMSG %1 :\x01PING %2\x01").arg(nick).arg(QDateTime::currentDateTime().toTime_t()));

            menu.addSeparator();
            QMenu* modeMenu = menu.addMenu("&Mode");

            action = modeMenu->addAction(tr("&Op"), this, SLOT(onActionTriggered()));
            action->setData(QString("MODE %2 +o %1").arg(nick).arg(d.receiver));

            action = modeMenu->addAction(tr("&Deop"), this, SLOT(onActionTriggered()));
            action->setData(QString("MODE %2 -o %1").arg(nick).arg(d.receiver));

            action = modeMenu->addAction(tr("&Voice"), this, SLOT(onActionTriggered()));
            action->setData(QString("MODE %2 +v %1").arg(nick).arg(d.receiver));

            action = modeMenu->addAction(tr("D&evoice"), this, SLOT(onActionTriggered()));
            action->setData(QString("MODE %2 -v %1").arg(nick).arg(d.receiver));

            QMenu* kickMenu = menu.addMenu("&Kick / Ban");

            action = kickMenu->addAction(tr("&Kick"), this, SLOT(onActionTriggered()));
            action->setData(QString("KICK %2 %1").arg(nick).arg(d.receiver));

            action = kickMenu->addAction(tr("&Ban"), this, SLOT(onActionTriggered()));
            action->setData(QString("MODE %2 +b %1").arg(nick).arg(d.receiver));

            menu.exec(contextMenuEvent->globalPos());
            return true;
        }
    }
    return false;
}

void MessageView::hideEvent(QHideEvent* event)
{
    QWidget::hideEvent(event);
    d.frameFind->hide();
}

void MessageView::onSend()
{
    QString input = d.lineEdit->text();
    if (!input.isEmpty())
    {
        d.lineEdit->clear();
        emit send(input);
    }
}

void MessageView::onTabPressed()
{
    if (!d.completer)
        return;

    // store selection
    int pos = d.lineEdit->cursorPosition();
    int start = d.lineEdit->selectionStart();
    QString selected = d.lineEdit->selectedText();

    // select current word
    d.lineEdit->cursorWordForward(false);
    d.lineEdit->cursorWordBackward(true);
    QString word = d.lineEdit->selectedText();

    QString prefix = d.completer->completionPrefix();
    if (prefix.isEmpty() || !word.startsWith(prefix, Qt::CaseInsensitive))
        d.completer->setCompletionPrefix(word);

    // restore selection
    d.lineEdit->setCursorPosition(pos);
    if (start != -1)
        d.lineEdit->setSelection(start, selected.length());

    // complete
    if (!word.isEmpty())
    {
        d.completer->complete();

        int count = d.completer->completionCount();
        if (count > 0)
        {
            int next = d.completer->currentRow() + 1;
            d.completer->setCurrentRow(next % count);
        }
    }
}

void MessageView::onTextEdited()
{
    if (d.completer)
        d.completer->setCompletionPrefix(QString());
}

void MessageView::insertCompletion(const QString& completion)
{
    if (!d.completer || d.completer->widget() != this)
        return;

    int pos = d.lineEdit->cursorPosition();
    QString text = d.lineEdit->text();
    if (pos > 0 && pos < text.length() && !text.at(pos - 1).isSpace())
        d.lineEdit->cursorWordForward(false);
    d.lineEdit->cursorWordBackward(true);
    pos = d.lineEdit->cursorPosition();

    QString tmp = completion;
    if (pos == 0 && !completion.startsWith("/"))
        tmp.append(":");
    d.lineEdit->insert(tmp);

    text = d.lineEdit->text();
    int cursor = d.lineEdit->cursorPosition();
    if (!text.at(cursor - 1).isSpace())
        d.lineEdit->insert(" ");
}

void MessageView::moveToNextPage()
{
    QScrollBar* scrollBar = d.textBrowser->verticalScrollBar();
    scrollBar->triggerAction(QScrollBar::SliderPageStepAdd);
}

void MessageView::moveToPrevPage()
{
    QScrollBar* scrollBar = d.textBrowser->verticalScrollBar();
    scrollBar->triggerAction(QScrollBar::SliderPageStepSub);
}

void MessageView::onAnchorClicked(const QUrl& link)
{
    if (link.scheme() == "query")
        emit query(link.path());
    else
        QDesktopServices::openUrl(link);
}

void MessageView::onActionTriggered()
{
    QAction* action = qobject_cast<QAction*>(sender());
    Q_ASSERT(action);
    emit command(action->data().toString());
}
